// $Id: server.c 40 2011-04-15 15:44:23Z cedric.shih@gmail.com $
/*
 * Copyright (C) 2011 Cedric Shih <cedric.shih@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#include "log.h"
#include "socket.h"
#include "future.h"

#include "server.h"

struct nioserver *nioserver_new(struct nioreactor *reactor,
		struct niochannel_factory *factory, void *cbarg)
{
	struct nioserver *server;

	if (!(server = calloc(1, sizeof(*server)))) {
		nioerror("failed to calloc");
		goto exception;
	}

	TAILQ_INIT(&server->clients);

	if (!(server->accept.future = niofuture_new(reactor))) {
		nioerror("failed to create future");
		goto exception;
	}

	server->reactor = reactor;
	memcpy(&server->factory, factory, sizeof(server->factory));
	server->cbarg = cbarg;
	return server;

exception:
	nioserver_free(server);
	return NULL;
}

void nioserver_free(struct nioserver *server)
{
	struct niochannel *client;

	if (!server) {
		return;
	}

	while ((client = TAILQ_FIRST(&server->clients))) {
		niochannel_close(client);
	}

	if (server->accept.future) {
		niofuture_free(server->accept.future);
	}

	if (server->accept.socket) {
		niosocket_free(server->accept.socket);
	}

	free(server);
}

int nioserver_is_bound(struct nioserver *server)
{
	return server->accept.socket != NULL;
}

int nioserver_set_timeout(struct nioserver *server,
		const struct timeval *timeout)
{
	int rc;

	if (!server || !timeout) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	memcpy(&server->tv, timeout, sizeof(server->tv));
	server->timeout = &server->tv;
	rc = 0;

finally:
	return rc;
}

int nioserver_get_timeout(struct nioserver *server,
		struct timeval *timeout)
{
	int rc;

	if (!server || !timeout) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	memcpy(timeout, &server->tv, sizeof(*timeout));
	rc = 0;

finally:
	return rc;
}

int nioserver_bind(struct nioserver *server,
		struct in_addr *address, unsigned short port, int backlog)
{
	int rc;

	if (!server || !address) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	if (nioserver_is_bound(server)) {
		nioerror("already bound");
		rc = NIO_EIO;
		goto finally;
	}

	niodebug("binding %s:%d", inet_ntoa(*address), port);

	if (!(server->accept.socket = niosocket_new())) {
		nioerror("failed to create socket");
		rc = NIO_ENOMEM;
		goto finally;
	}

	if ((rc = niosocket_listen(server->accept.socket, address, port,
			backlog))) {
		nioerror("failed to bind: %s", nio_strerror(rc));
		goto finally;
	}

	if ((rc = niofuture_readable(server->accept.future,
			server->accept.socket, NULL, nioserver_on_acceptable,
			server))) {
		nioerror("failed to listen on readable: %s", nio_strerror(rc));
		goto finally;
	}

	rc = 0;

finally:
	if (rc != 0) {
		if (server->accept.socket != NULL) {
			niosocket_free(server->accept.socket);
			server->accept.socket = NULL;
		}
	}
	return rc;

}

int nioserver_unbind(struct nioserver *server)
{
	int rc;
	struct niochannel *client;

	if (!server) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	if (!nioserver_is_bound(server)) {
		nioerror("not bound");
		rc = NIO_EIO;
		goto finally;
	}

	niodebug("closing all channels");

	while ((client = TAILQ_FIRST(&server->clients))) {
		niochannel_close(client);
	}

	niodebug("unbinding %s:%d", inet_ntoa(server->accept.socket->address),
			server->accept.socket->port);

	niofuture_cancel(server->accept.future);

	if (server->accept.socket) {
		niosocket_free(server->accept.socket);
		server->accept.socket = NULL;
	}

	rc = 0;

finally:
	return rc;
}

void nioserver_on_channel_close(struct niochannel *channel,
		struct niosocket *socket, void *cbarg)
{
	struct nioserver *server = cbarg;

	niodebug("channel closed: %s:%d",
			inet_ntoa(socket->address), socket->port);

	niosocket_free(socket);
	nioserver_remove(server, channel);
}

void nioserver_on_acceptable(struct niofuture *future,
		struct niosocket *socket, int flags, void *cbarg)
{
	int rc;
	struct niosocket *accepted;
	struct niochannel *client = NULL;
	struct nioserver *server = cbarg;

	if (!(accepted = niosocket_new())) {
		rc = ENOMEM;
		nioerror("failed to create socket");
		goto finally;
	}

	if ((rc = niosocket_accept(server->accept.socket, accepted))) {
		nioerror("failed to accept: %s", nio_strerror(rc));
		goto finally;
	}

	if (!(client = (*server->factory.create)(accepted, server->cbarg))) {
		rc = ENOMEM;
		nioerror("failed to create channel");
		goto finally;
	}

	TAILQ_INSERT_TAIL(&server->clients, client, entry);

	if ((rc = niochannel_open(client, accepted, server->timeout,
			nioserver_on_channel_close, server))) {
		nioerror("failed to open: %s", nio_strerror(rc));
		goto finally;
	}

	rc = 0;

finally:
	if (rc != 0) {
		if (client != NULL) {
			(*server->factory.release)(client, server->cbarg);
		}
		if (accepted != NULL) {
			niosocket_free(accepted);
		}
	}

	if ((rc = niofuture_readable(server->accept.future,
			server->accept.socket, NULL, nioserver_on_acceptable,
			server))) {
		nioerror("failed to listen on readable: %s", nio_strerror(rc));
		goto finally;
	}
	return;
}

void nioserver_remove(struct nioserver *server, struct niochannel *client)
{
	niodebug("removing client: %p", client);

	TAILQ_REMOVE(&server->clients, client, entry);

	if (client != NULL) {
		(*server->factory.release)(client, server->cbarg);
	}
}
